package com.blockchain.API;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import org.web3j.abi.FunctionEncoder;
import org.web3j.abi.FunctionReturnDecoder;
import org.web3j.abi.TypeReference;
import org.web3j.abi.datatypes.*;
import org.web3j.abi.datatypes.generated.Bytes32;
import org.web3j.abi.datatypes.generated.Uint256;
import org.web3j.crypto.Credentials;
import org.web3j.crypto.ECKeyPair;
import org.web3j.crypto.Keys;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.DefaultBlockParameterName;
import org.web3j.protocol.core.methods.response.EthGetBalance;
import org.web3j.protocol.core.methods.response.EthGetTransactionCount;
import org.web3j.protocol.core.methods.response.EthSendTransaction;
import org.web3j.protocol.core.methods.response.TransactionReceipt;
import org.web3j.protocol.http.HttpService;
import org.web3j.tx.RawTransactionManager;
import org.web3j.tx.response.PollingTransactionReceiptProcessor;
import org.web3j.tx.response.TransactionReceiptProcessor;
import org.web3j.utils.Convert;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * 时间币交易、溯源等的API
 * 需使用 web3j 库
 */
public class TimeCoinAPI {

    // 梦开始的地方
    private static final Web3j web3j = Web3j.build(new HttpService("http://47.108.212.117:8545"));
    // 合约所在地址
    private static final String contractAddress = "0x43d1f9096674b5722d359b6402381816d5b22f28";
    // 暂时没考虑安全性
    public static String adminPK = "0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63";
    //    // 管理员地址
//    private static String adminAddress = null;
    // 最大gas
    private static final BigInteger gasLimit = BigInteger.valueOf(52401314L);
    // 每单位gas的价格
    private static final BigInteger gasPrice = BigInteger.valueOf(0L);
    // 小费
    private static final BigInteger value = BigInteger.valueOf(0L);
    // 连chainId都是爱你的形状
    private static final Long chainId = 5201314L;

    /**
     * 给用户分配一个私钥
     * TODO 用户注册的时候调用，一定将返回的私钥存在数据库中，后面能不能交易就看你存不存了
     * TODO 你就说你存不存吧！！
     *
     * @return 私钥
     */
    public static String userRegister() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException {
        // 创建新的EC密钥对
        ECKeyPair ecKeyPair = Keys.createEcKeyPair();
        // 返回私钥
        return ecKeyPair.getPrivateKey().toString(16);
    }

    /**
     * 查询用户是否被分配过时间币
     *
     * @param callerPK 发起查询的用户的私钥
     * @param userPK   要查询的用户的私钥
     * @return 是否被分配过。"true"|"false"
     */
    public static boolean isAwardedCoin(String callerPK, String userPK) throws Exception {
        List<Type> someTypes = contractCall(callerPK, "isAwardedCoin", Arrays.asList(new Address(getAddress(userPK))), Arrays.asList(new TypeReference<Bool>() {
        }));
        String outcome = someTypes.get(0).getValue().toString();
        return outcome.equals("true");
    }

    /**
     * 查询总时间币的数量
     *
     * @param callerPK 发起查询的用户的私钥
     * @return 时间币数量
     */
    public static String totalCoin(String callerPK) throws Exception {
        List<Type> someTypes = contractCall(callerPK, "totalCoin", Collections.emptyList(), Arrays.asList(new TypeReference<Uint>() {
        }));
        return someTypes.get(0).getValue().toString();
    }

    /**
     * 获取私钥为 usrPK 的用户的时间币数量
     *
     * @param callerPK 发起操作的用户的私钥
     * @param usrPK    要查询剩余时间币的用户的私钥
     * @return 用户的时间币数量
     */
    public static String balanceOfCoin(String callerPK, String usrPK) throws Exception {
        List<Type> someTypes = contractCall(callerPK, "balanceOfCoin", Arrays.asList(new Address(getAddress(usrPK))), Arrays.asList(new TypeReference<Uint>() {
        }));
        return someTypes.get(0).getValue().toString();
    }

    /**
     * 获取私钥为 usrPK 的用户的所有时间币id
     *
     * @param callerPK 发起操作的用户的私钥
     * @param usrPK    要查询剩余时间币的用户的私钥
     * @return 用户的所有时间币id。"用户没有时间币" 或者 "1 2 3 4 "（空格隔开）
     */
    public static String coinOfUser(String callerPK, String usrPK) throws Exception {
        int num = Integer.parseInt(balanceOfCoin(callerPK, usrPK));  // 用户拥有的时间币数量
        if (num == 0) {
            return "用户没有时间币";
        }
        StringBuilder coinIds = new StringBuilder();
        StringBuilder coinHashes = new StringBuilder();
        // 获取用户拥有的时间币
        for (int i = 0; i < num; i++) {
            coinIds.append(coinOfOwnerByIndex(callerPK, usrPK, BigInteger.valueOf(i))).append(" ");
            coinHashes.append(coinOfOwnerByHash(callerPK, usrPK, BigInteger.valueOf(i))).append(" ");
        }
        return coinIds.toString() + "@" + coinHashes.toString();
    }

    /**
     * 给私钥为usrPK的用户发行 num 个时间币
     *
     * @param callerPK 传入管理员的私钥
     * @param usrPK    传入用户的私钥
     * @param num      发行的时间币个数
     */
    public static void awardCoin(String callerPK, String usrPK, int num) throws Exception {
        if (num <= 0) {
            throw new Exception("发行的数量必须大于0");
        }
        String ok = contractTransction(callerPK, "awardCoin", Arrays.asList(new Address(getAddress(usrPK)), new Uint(BigInteger.valueOf(num))), Collections.emptyList());
        if (ok.equals("-1")) {
            throw new Exception("交换时间币失败");
        } else {
            int totalCoin = Integer.parseInt(totalCoin(callerPK));
            String URI = createInitJson(getAddress(usrPK), BigInteger.valueOf(Integer.parseInt(getCoinBlockHeight(callerPK))), ok);
            List<BigInteger> coinIds = new ArrayList<>();
            List<String> newURI = new ArrayList<>();
            for (int i = 0; i < num; i++) {
                coinIds.add(BigInteger.valueOf(totalCoin - 1 - i));
                newURI.add(URI);
            }
            setCoinInfo(callerPK, coinIds, newURI);
        }
    }

    /***
     * 从 fromPK 转移一个时间币给 toPK
     * @param callerPK 交易发起者的私钥
     * @param fromPK 被转移者的私钥
     * @param toPK 收获者的私钥
     * @param coinIds 要进行转移的时间币的 id 数组
     * @param serviceId 服务id
     * @param transactionType 服务类型
     */
    public static void transferCoin(String callerPK, String fromPK, String toPK, List<BigInteger> coinIds, int serviceId, String transactionType) throws Exception {
        // 将BigInteger列表转换为Uint256列表
        List<Uint256> uintCoinIds = new ArrayList<>();
        for (BigInteger coinId : coinIds) {
            uintCoinIds.add(new Uint256(coinId));
        }
        // 创建一个DynamicArray对象
        DynamicArray<Uint256> dynamicArray = new DynamicArray<>(Uint256.class, uintCoinIds);

        String ok = contractTransction(callerPK, "transferCoin", Arrays.asList(new Address(getAddress(fromPK)), new Address(getAddress(toPK)), dynamicArray));
        if (ok.equals("-1")) {
            throw new Exception("交换时间币失败");
        } else {
            String owner = getAddress(toPK);
            List<String> newURI = new ArrayList<>();
            for (BigInteger coinId : coinIds) {
                String newInfo = createNewJson(owner, BigInteger.valueOf(Integer.parseInt(getCoinBlockHeight(callerPK))), ok, BigInteger.valueOf(serviceId), transactionType, coinId);
                newURI.add(newInfo);
            }
            setCoinInfo(callerPK, coinIds, newURI);
        }
    }

    /**
     * 对时间币进行溯源
     *
     * @param callerPK 发起者的私钥
     * @param coinId   时间币的 id
     * @return 返回一个字符串，含溯源信息
     */
    public static String getCoinInfo(String callerPK, BigInteger coinId) throws Exception {
        List<Type> someTypes = contractCall(callerPK, "getCoinInfo", Arrays.asList(new Uint(coinId)), Arrays.asList(new TypeReference<DynamicArray<Utf8String>>() {}));
        DynamicArray<Utf8String> dynamicArray = (DynamicArray<Utf8String>) someTypes.get(0);
        // 转换为Java的List
        List<Utf8String> utf8StringList = dynamicArray.getValue();
        // 将Utf8String转换为String
        List<String> infos = utf8StringList.stream()
                .map(Utf8String::getValue)
                .toList();

        String info = initJson(infos.get(0));
        for (int i = 1; i < infos.size(); i++) {
            info = concatJson(info, infos.get(i), i);
        }
        return info;
    }

    public static String getCoinInfoByHash(String callerPK, String coinHash) throws Exception {
        // 准备函数参数
        coinHash = coinHash.replaceFirst("^0x", "");
        BigInteger bi = new BigInteger(coinHash, 16);
        byte[] byteValue = bi.toByteArray();
        byte[] byteValPadded = new byte[32];
        if (byteValue[0] == 0) {
            System.arraycopy(byteValue, 1, byteValPadded, 32 - byteValue.length + 1, byteValue.length - 1);
        } else {
            System.arraycopy(byteValue, 0, byteValPadded, 32 - byteValue.length, byteValue.length);
        }

        List<Type> someTypes = contractCall(callerPK, "getCoinInfoByHash", Arrays.asList(new Bytes32(byteValPadded)), Arrays.asList(new TypeReference<DynamicArray<Utf8String>>() {}));
        DynamicArray<Utf8String> dynamicArray = (DynamicArray<Utf8String>) someTypes.get(0);
        // 转换为Java的List
        List<Utf8String> utf8StringList = dynamicArray.getValue();
        // 将Utf8String转换为String
        List<String> infos = utf8StringList.stream()
                .map(Utf8String::getValue)
                .toList();

        String info = initJson(infos.get(0));
        for (int i = 1; i < infos.size(); i++) {
            info = concatJson(info, infos.get(i), i);
        }
        return info;
    }

    /**
     * 获取某个时间币的溯源信息
     *
     * @param callerPK 发起人的私钥
     * @param coinId   要查询的币的id
     * @return 交易的次数
     */
    public static String getCoinTransferCnt(String callerPK, BigInteger coinId) throws Exception {
        List<Type> someTypes = contractCall(callerPK, "getCoinTransferCnt", Arrays.asList(new Uint(coinId)), Arrays.asList(new TypeReference<Utf8String>() {
        }));
        return someTypes.get(0).getValue().toString();
    }

    private static String createInitJson(String owner, BigInteger blockHeight, String transactionHash) {
        // 创建一个新的 JSONObject
        JsonObject obj = new JsonObject();
        // 添加键值对
        obj.addProperty("time", ZonedDateTime.now().toString());
        obj.addProperty("owner", owner);
        obj.addProperty("transactionType", "发行时间币");
        obj.addProperty("blockHeight", blockHeight);
        obj.addProperty("transactionHash", transactionHash);
        // 将 JSONObject 转换为字符串
        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        // 将 JSONObject 转换为字符串
        return gson.toJson(obj);
    }

    private static String createNewJson(String owner, BigInteger blockHeight, String transactionHash, BigInteger serviceId, String transactionType, BigInteger coinId) {
        // 创建一个新的 JSONObject
        JsonObject obj = new JsonObject();
        // 添加键值对
        obj.addProperty("time", ZonedDateTime.now().toString());
        obj.addProperty("owner", owner);
        obj.addProperty("transactionType", transactionType);
        obj.addProperty("blockHeight", blockHeight.toString());
        obj.addProperty("transactionHash", transactionHash);
        obj.addProperty("serviceId", serviceId.toString());
        obj.addProperty("coinId", coinId.toString());

        // 将 JSONObject 转换为字符串
        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        // 将 JSONObject 转换为字符串
        return gson.toJson(obj);
    }

    private static String initJson(String json) {
        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        JsonObject obj = gson.fromJson(json, JsonObject.class);
        JsonObject outer = new JsonObject();
        outer.add("0", obj);
        return gson.toJson(outer);
    }

    private static String concatJson(String json1, String json2, int cnt) {

        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        JsonObject obj1 = gson.fromJson(json1, JsonObject.class);
        JsonObject obj2 = gson.fromJson(json2, JsonObject.class);

        // 将上面创建的 JSONObject 作为值添加进去
        obj1.add(String.valueOf(cnt), obj2);
        return gson.toJson(obj1);
    }

    // 获取当前区块高度
    private static String getCoinBlockHeight(String callerPK) throws Exception {
        List<Type> someTypes = contractCall(callerPK, "getCoinBlockHeight", Collections.emptyList(), Arrays.asList(new TypeReference<Uint>() {
        }));
        return someTypes.get(0).getValue().toString();
    }

    // 获取时间币的主人
    private static String getCoinOwner(String callerPK, BigInteger coinId) throws Exception {
        List<Type> someTypes = contractCall(callerPK, "getCoinOwner", Arrays.asList(new Uint(coinId)), Arrays.asList(new TypeReference<Utf8String>() {
        }));
        return someTypes.get(0).getValue().toString();
    }

    // 添加时间币数据
    private static void setCoinInfo(String callerPK, List<BigInteger> coinIds, List<String> newURI) throws Exception {
        if (coinIds.size() != newURI.size()) {
            throw new Exception("coinIds和newURI必须是同长度");
        }
        // 将BigInteger列表转换为Uint256列表
        List<Uint256> uintCoinIds = new ArrayList<>();
        for (BigInteger coinId : coinIds) {
            uintCoinIds.add(new Uint256(coinId));
        }

        // 创建一个DynamicArray对象
        DynamicArray<Uint256> dynamicArrayCoinIds = new DynamicArray<>(Uint256.class, uintCoinIds);
        // 将String列表转换为Utf8String列表
        List<Utf8String> stringNewURI = new ArrayList<>();
        for (String uri : newURI) {
            stringNewURI.add(new Utf8String(uri));
        }
        // 创建一个DynamicArray对象
        DynamicArray<Utf8String> dynamicArrayURIs = new DynamicArray<>(Utf8String.class, stringNewURI);

        String ok = contractTransction(callerPK, "setCoinInfo", Arrays.asList(dynamicArrayCoinIds, dynamicArrayURIs));

//        String ok = contractTransction(callerPK, "setCoinInfo",
//                Arrays.<Type>asList(
//                        new DynamicArray<Uint256>(
//                                Uint256.class,
//                                org.web3j.abi.Utils.typeMap(coinIds, Uint256.class)),
//                        new DynamicArray<Utf8String>(
//                                Utf8String.class,
//                                org.web3j.abi.Utils.typeMap(newURI, Utf8String.class))),
//                Collections.<TypeReference<?>>emptyList());

        if (ok.equals("-1")) {
            throw new Exception("设置溯源信息失败");
        }
    }

    // 获取用户的第 index 个时间币的 id
    private static String coinOfOwnerByIndex(String callerPK, String usrPK, BigInteger index) throws Exception {
        List<Type> someTypes = contractCall(callerPK, "coinOfOwnerByIndex", Arrays.asList(new Address(getAddress(usrPK)), new Uint(index)), Arrays.asList(new TypeReference<Uint>() {
        }));
        return someTypes.get(0).getValue().toString();
    }

    // 获取用户的第 index 个时间币的 哈希值
    private static String coinOfOwnerByHash(String callerPK, String usrPK, BigInteger index) throws Exception {
        List<Type> someTypes = contractCall(callerPK, "coinOfOwnerByHash", Arrays.asList(new Address(getAddress(usrPK)), new Uint(index)), Arrays.asList(new TypeReference<Bytes32>() {
        }));
        return bytesToHex((byte[]) someTypes.get(0).getValue());
    }

    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder("0x");
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    // 合约交易模板
    private static String contractTransction(String callerPK, String funName, List<Type> inputParams, List<TypeReference<?>> outputParams) throws Exception {
        // 获取凭证
        Credentials credentials = Credentials.create(callerPK);
        // 创建函数
        Function function = new Function(
                funName,
                inputParams,
                outputParams
        );
        // 解码
        String encodedFunction = FunctionEncoder.encode(function);
        // 发送交易
        RawTransactionManager rawManager = new RawTransactionManager(web3j, credentials, chainId);
        EthSendTransaction ethSendTransaction = rawManager.sendTransaction(gasPrice, gasLimit, contractAddress, encodedFunction, value);
        // 等待交易被挖矿
        TransactionReceiptProcessor receiptProcessor = new PollingTransactionReceiptProcessor(web3j, 500, 60);
        TransactionReceipt receipt = receiptProcessor.waitForTransactionReceipt(ethSendTransaction.getTransactionHash());
        // 返回交易哈希
        if (receipt.isStatusOK()) {
            return receipt.getTransactionHash();
        }
        return "-1";
    }

    // 重载交易
    private static String contractTransction(String callerPK, String funName, List<Type> inputParams) throws Exception {
        return contractTransction(callerPK, funName, inputParams, Collections.emptyList());
    }

    // 合约调用模板
    private static List<Type> contractCall(String callerPK, String funName, List<Type> inputParams, List<TypeReference<?>> outputParams) throws Exception {
        // 获取凭证
        Credentials credentials = Credentials.create(callerPK);
        // 创建函数
        Function function = new Function(
                funName,
                inputParams,
                outputParams
        );
        // 解码
        String encodedFunction = FunctionEncoder.encode(function);
        // 发送交易
        RawTransactionManager rawTransactionManager = new RawTransactionManager(web3j, credentials, chainId);
        String out = rawTransactionManager.sendCall(contractAddress, encodedFunction, DefaultBlockParameterName.LATEST);
        // 解码返回值
        List<Type> someTypes = FunctionReturnDecoder.decode(out, function.getOutputParameters());
        // 返回结果
        return someTypes;
    }

    // 查询交易次数
    private static BigInteger getNonce(String walletAddress) throws IOException {
        EthGetTransactionCount ethGetTransactionCount = web3j.ethGetTransactionCount(walletAddress, DefaultBlockParameterName.LATEST).send();
        return ethGetTransactionCount.getTransactionCount();
    }

    // 查询地址
    private static String getAddress(String userPK) {
        return Credentials.create(userPK).getAddress();
    }

    // 查询以太币余额
    private static void getBalance(String walletAddress) throws IOException {
        EthGetBalance balanceWei = web3j.ethGetBalance(walletAddress, DefaultBlockParameterName.LATEST).send();
        System.out.println("balance in wei: " + balanceWei.getBalance());
        BigDecimal balanceInEther = Convert.fromWei(balanceWei.getBalance().toString(), Convert.Unit.ETHER);
        System.out.println("balance in ether: " + balanceInEther);
    }
}
